Appearance
JavaScript-TypeScript 循环依赖完全指南
本节目标:理解“JavaScript-TypeScript 循环依赖完全指南”的核心思路,并能把它用于实际开发或面试表达。
** 📚 **目录
1. 什么是循环依赖
1.1 定义
循环依赖是指两个或多个模块相互依赖,形成一个闭环的依赖关系。
javascript
// 简单循环依赖
// fileA.js
import { funcB } from './fileB.js'
export const funcA = () => funcB()
// fileB.js
import { funcA } from './fileA.js'
export const funcB = () => funcA()1.2 循环依赖的类型
直接循环依赖
javascript
// userStore.js
import { resetAllStores } from './index.js'
export const logout = () => resetAllStores()
// index.js
import { userStore } from './userStore.js'
export const resetAllStores = () => userStore.reset()间接循环依赖
javascript
// A.js → B.js → C.js → A.js
// A.js
import { B } from './B.js'
// B.js
import { C } from './C.js'
// C.js
import { A } from './A.js'条件循环依赖
javascript
// utils.js
import { devConfig } from './config.js'
// config.js
if (process.env.NODE_ENV === 'development') {
import { debugUtils } from './utils.js' // 动态导入形成循环
}2. 各种模块系统的处理机制
2.1 CommonJS (Node.js)
处理机制
javascript
// CommonJS 使用缓存机制处理循环依赖
// moduleA.js
console.log('A: 开始加载')
const { valueB } = require('./moduleB')
console.log('A: B的值是', valueB)
module.exports = { valueA: 'A的值' }
console.log('A: 加载完成')
// moduleB.js
console.log('B: 开始加载')
const { valueA } = require('./moduleA') // 此时A还没完全加载完
console.log('B: A的值是', valueA) // undefined
module.exports = { valueB: 'B的值' }
console.log('B: 加载完成')输出结果:
A: 开始加载
B: 开始加载
B: A的值是 undefined
B: 加载完成
A: B的值是 B的值
A: 加载完成CommonJS的缓存机制
javascript
// require.cache 存储已加载的模块
console.log(Object.keys(require.cache))
// 模块只会被执行一次
require('./moduleA') // 第一次:执行模块代码
require('./moduleA') // 第二次:从缓存返回2.2 ES Modules (ESM)
处理机制
javascript
// ESM 使用"实时绑定"(Live Binding)
// moduleA.mjs
console.log('A: 开始加载')
import { valueB } from './moduleB.mjs'
console.log('A: B的值是', valueB)
export const valueA = 'A的值'
console.log('A: 加载完成')
// moduleB.mjs
console.log('B: 开始加载')
import { valueA } from './moduleA.mjs'
console.log('B: A的值是', valueA)
export const valueB = 'B的值'
console.log('B: 加载完成')三阶段加载过程
- 构建阶段:解析模块依赖图
- 实例化阶段:创建模块记录,建立绑定
- 求值阶段:执行模块代码
javascript
// 在实例化阶段,所有导出都已经"声明"但未"初始化"
// 这允许循环引用,但可能访问到未初始化的值2.3 AMD (RequireJS)
javascript
// AMD 允许异步加载,可以处理某些循环依赖
define(['./moduleB'], function (moduleB) {
return {
valueA: 'A的值',
callB: function () {
return moduleB.valueB
},
}
})2.4 UMD (Universal Module Definition)
javascript
// UMD 兼容多种模块系统,继承各自的循环依赖处理方式
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['./moduleB'], factory) // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('./moduleB')) // CommonJS
} else {
root.moduleA = factory(root.moduleB) // 全局变量
}
})(typeof self !== 'undefined' ? self : this, function (moduleB) {
return { valueA: 'A的值' }
})3. 构建工具的处理方式
3.1 Webpack
检测和警告
javascript
// webpack.config.js
module.exports = {
stats: {
warningsFilter: (warning) => {
// 过滤循环依赖警告
return warning.includes('Circular dependency detected')
},
},
}Webpack的处理策略
javascript
// Webpack 会:
// 1. 检测循环依赖并发出警告
// 2. 尝试解决简单的循环依赖
// 3. 在某些情况下可能导致打包失败
// 示例警告信息:
/*
WARNING in Circular dependency detected:
src/store/index.ts -> src/store/user.ts -> src/store/index.ts
*/Webpack插件处理
javascript
// 使用 circular-dependency-plugin
const CircularDependencyPlugin = require('circular-dependency-plugin')
module.exports = {
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
failOnError: true, // 发现循环依赖时构建失败
allowAsyncCycles: false,
cwd: process.cwd(),
}),
],
}3.2 Vite
基于Rollup的处理
javascript
// vite.config.js
export default {
build: {
rollupOptions: {
// Rollup 默认会警告循环依赖
onwarn(warning, warn) {
if (warning.code === 'CIRCULAR_DEPENDENCY') {
console.warn(`循环依赖: ${warning.message}`)
return
}
warn(warning)
},
},
},
}开发模式vs生产模式
javascript
// 开发模式(基于浏览器原生ESM)
// - 循环依赖可能导致模块未定义
// - 热更新可能出现问题
// 生产模式(Rollup打包)
// - 更严格的循环依赖检测
// - 可能导致打包失败3.3 TypeScript编译器
typescript
// tsconfig.json
{
"compilerOptions": {
"noImplicitReturns": true,
"strict": true
},
// TS编译器会检测一些循环依赖,但不是所有
}
// TS特有的循环依赖问题
interface UserType {
posts: PostType[]
}
interface PostType {
author: UserType // 接口循环依赖,但这是允许的
}4. 循环依赖的主要表现
4.1 未定义行为
javascript
// store/user.js
import { resetStore } from './index.js'
export const user = {
logout() {
resetStore() // 可能调用时resetStore是undefined
},
}
// store/index.js
import { user } from './user.js'
export const resetStore = () => {
user.reset() // user可能是undefined
}4.2 初始化顺序问题
javascript
// config.js
import { apiClient } from './api.js'
export const config = {
baseURL: '[https://api.example.com](https://api.example.com)',
client: apiClient, // 可能是undefined
}
// api.js
import { config } from './config.js'
export const apiClient = createClient(config.baseURL) // config.baseURL可能undefined4.3 热更新异常
javascript
// 在Vite/Webpack的HMR中,循环依赖可能导致:
// 1. 模块更新失败
// 2. 状态丢失
// 3. 组件重复渲染
// 4. 内存泄漏4.4 TypeScript类型问题
typescript
// types/user.ts
import { Post } from './post'
export interface User {
id: string
posts: Post[]
}
// types/post.ts
import { User } from './user' // 循环依赖
export interface Post {
id: string
author: User
}
// 可能导致:
// - 类型推断失败
// - 编译错误
// - IDE智能提示异常5. 如何发现循环依赖
5.1 编译时发现
控制台警告
bash
# Webpack警告
WARNING in Circular dependency detected:
src/components/UserProfile.vue ->
src/store/user.js ->
src/store/index.js ->
src/components/UserProfile.vue
# Rollup/Vite警告
(!) Circular dependency:
src/utils/api.js -> src/utils/auth.js -> src/utils/api.jsTypeScript错误
typescript
// 某些情况下TS会报错
// error TS2345: Argument of type 'typeof import("...")'
// is not assignable to parameter of type 'never'5.2 运行时发现
控制台错误
javascript
// TypeError: Cannot read properties of undefined (reading 'someMethod')
// ReferenceError: Cannot access 'someVariable' before initialization
// TypeError: someFunction is not a function调试技巧
javascript
// 在模块顶部添加日志
console.log('Loading module:', __filename || import.meta.url)
// 检查导入的值
import { someFunction } from './other-module'
console.log('Imported function:', typeof someFunction)5.3 行为异常
javascript
// 1. 函数调用失败
someImportedFunction() // TypeError: someImportedFunction is not a function
// 2. 对象属性缺失
console.log(importedObject.property) // undefined
// 3. 类实例化失败
new ImportedClass() // TypeError: ImportedClass is not a constructor
// 4. 状态不一致
// 某些状态更新没有生效,或者产生了意外的副作用6. 检测工具和方法
6.1 静态分析工具
madge - 依赖分析神器
bash
# 安装
npm install -g madge
# 检测循环依赖
madge --circular src/
# 生成依赖图
madge --image deps.svg src/
# 检测特定文件
madge --circular --extensions ts,tsx,js,jsx src/
# JSON输出
madge --circular --format json src/ > circular-deps.jsondependency-cruiser
bash
# 安装
npm install -g dependency-cruiser
# 生成配置
depcruise --init
# 检测循环依赖
depcruise --validate .dependency-cruiser.js src/6.2 ESLint规则
javascript
// .eslintrc.js
module.exports = {
plugins: ['import'],
rules: {
'import/no-cycle': [
'error',
{
maxDepth: 10, // 检测深度
ignoreExternal: true, // 忽略外部模块
},
],
'import/no-self-import': 'error',
'import/no-useless-path-segments': 'error',
},
}6.3 自定义检测脚本
javascript
// detect-circular.js
const fs = require('fs')
const path = require('path')
class CircularDetector {
constructor(rootDir) {
this.rootDir = rootDir
this.dependencies = new Map()
this.visiting = new Set()
this.visited = new Set()
}
detectCircular() {
this.buildDependencyGraph()
return this.findCycles()
}
buildDependencyGraph() {
const files = this.getAllFiles(this.rootDir)
files.forEach((file) => {
const deps = this.extractDependencies(file)
this.dependencies.set(file, deps)
})
}
extractDependencies(filePath) {
const content = fs.readFileSync(filePath, 'utf8')
const importRegex = /(?:import|from)\s+['"]([^'"]+)['"]/g
const dependencies = []
let match
while ((match = importRegex.exec(content)) !== null) {
const dep = this.resolvePath(filePath, match[1])
if (dep) dependencies.push(dep)
}
return dependencies
}
findCycles() {
const cycles = []
for (const [file] of this.dependencies) {
if (!this.visited.has(file)) {
const cycle = this.dfs(file, [])
if (cycle) cycles.push(cycle)
}
}
return cycles
}
dfs(file, path) {
if (this.visiting.has(file)) {
// 找到循环
const cycleStart = path.indexOf(file)
return path.slice(cycleStart).concat(file)
}
if (this.visited.has(file)) return null
this.visiting.add(file)
path.push(file)
const deps = this.dependencies.get(file) || []
for (const dep of deps) {
const cycle = this.dfs(dep, [...path])
if (cycle) return cycle
}
this.visiting.delete(file)
this.visited.add(file)
return null
}
}
// 使用
const detector = new CircularDetector('./src')
const cycles = detector.detectCircular()
console.log('发现的循环依赖:', cycles)6.4 构建时检测
javascript
// webpack-plugin-circular-dependency.js
class CircularDependencyDetector {
apply(compiler) {
compiler.hooks.compilation.tap('CircularDependencyDetector', (compilation) => {
compilation.hooks.finishModules.tap('CircularDependencyDetector', (modules) => {
const cycles = this.detectCycles(modules)
if (cycles.length > 0) {
cycles.forEach((cycle) => {
compilation.warnings.push(new Error(`循环依赖: ${cycle.join(' -> ')}`))
})
}
})
})
}
detectCycles(modules) {
// 实现循环检测逻辑
return []
}
}
module.exports = CircularDependencyDetector7. 解决循环依赖的策略
7.1 依赖注入 (Dependency Injection)
javascript
// 问题代码
// userService.js
import { emailService } from './emailService.js'
export const userService = {
createUser(data) {
const user = createUserInDB(data)
emailService.sendWelcomeEmail(user.email) // 直接依赖
return user
},
}
// emailService.js
import { userService } from './userService.js' // 循环依赖!
export const emailService = {
sendWelcomeEmail(email) {
// 需要用户信息
const user = userService.getUserByEmail(email)
// ...
},
}
// 解决方案:依赖注入
// userService.js
export const createUserService = (emailService) => ({
createUser(data) {
const user = createUserInDB(data)
emailService.sendWelcomeEmail(user.email)
return user
},
})
// emailService.js
export const createEmailService = (userService) => ({
sendWelcomeEmail(email) {
const user = userService.getUserByEmail(email)
// ...
},
})
// container.js - 组装依赖
import { createUserService } from './userService.js'
import { createEmailService } from './emailService.js'
// 创建容器
const services = {}
services.emailService = createEmailService(() => services.userService)
services.userService = createUserService(services.emailService)
export { services }7.2 事件系统解耦
javascript
// 问题代码:直接调用形成循环
// orderService.js
import { inventoryService } from './inventoryService.js'
export const orderService = {
createOrder(items) {
inventoryService.reserveItems(items) // 直接调用
},
}
// inventoryService.js
import { orderService } from './orderService.js' // 循环依赖!
export const inventoryService = {
updateStock(item) {
orderService.notifyStockChange(item) // 直接调用
},
}
// 解决方案:事件系统
// eventBus.js
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) this.events[event] = []
this.events[event].push(callback)
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach((callback) => callback(data))
}
}
}
export const eventBus = new EventBus()
// orderService.js
import { eventBus } from './eventBus.js'
export const orderService = {
createOrder(items) {
eventBus.emit('order:reserve-items', items) // 发送事件
},
init() {
eventBus.on('inventory:stock-changed', this.handleStockChange)
},
}
// inventoryService.js
import { eventBus } from './eventBus.js'
export const inventoryService = {
reserveItems(items) {
// 处理库存预留
},
updateStock(item) {
eventBus.emit('inventory:stock-changed', item) // 发送事件
},
init() {
eventBus.on('order:reserve-items', this.reserveItems)
},
}7.3 抽取公共依赖
javascript
// 问题代码
// userStore.js
import { postStore } from './postStore.js'
export const userStore = {
getUserPosts(userId) {
return postStore.getPostsByUser(userId)
}
}
// postStore.js
import { userStore } from './userStore.js' // 循环依赖!
export const postStore = {
createPost(post) {
const user = userStore.getUser(post.userId)
// ...
}
}
// 解决方案:抽取公共依赖
// types.js
export interface User {
id: string
name: string
}
export interface Post {
id: string
userId: string
content: string
}
// dataService.js - 公共数据访问层
export const dataService = {
async getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
},
async getPosts(userId) {
return fetch(`/api/users/${userId}/posts`).then(r => r.json())
},
async createPost(post) {
return fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(post)
}).then(r => r.json())
}
}
// userStore.js
import { dataService } from './dataService.js'
export const userStore = {
getUserPosts(userId) {
return dataService.getPosts(userId) // 通过公共层访问
}
}
// postStore.js
import { dataService } from './dataService.js'
export const postStore = {
async createPost(post) {
const user = await dataService.getUser(post.userId) // 通过公共层访问
return dataService.createPost(post)
}
}7.4 延迟导入 (Lazy Import)
javascript
// 问题代码:顶层导入形成循环
// moduleA.js
import { funcB } from './moduleB.js'
export const funcA = () => {
return funcB() // 立即调用
}
// moduleB.js
import { funcA } from './moduleA.js' // 循环依赖!
export const funcB = () => {
return funcA()
}
// 解决方案1:动态导入
// moduleA.js
export const funcA = async () => {
const { funcB } = await import('./moduleB.js') // 运行时导入
return funcB()
}
// moduleB.js
export const funcB = async () => {
const { funcA } = await import('./moduleA.js') // 运行时导入
return funcA()
}
// 解决方案2:惰性函数
// moduleA.js
let funcB
export const funcA = () => {
if (!funcB) {
funcB = require('./moduleB.js').funcB // 延迟加载
}
return funcB()
}7.5 分层架构
javascript
// 清晰的分层架构避免循环依赖
// 数据层 (Data Layer)
// models/User.js
export class User {
constructor(data) {
this.id = data.id
this.name = data.name
}
}
// repositories/UserRepository.js
import { User } from '../models/User.js'
export class UserRepository {
async findById(id) {
const data = await fetch(`/api/users/${id}`)
return new User(data)
}
}
// 业务层 (Business Layer)
// services/UserService.js
import { UserRepository } from '../repositories/UserRepository.js'
export class UserService {
constructor() {
this.userRepository = new UserRepository()
}
async getUser(id) {
return this.userRepository.findById(id)
}
}
// 表现层 (Presentation Layer)
// controllers/UserController.js
import { UserService } from '../services/UserService.js'
export class UserController {
constructor() {
this.userService = new UserService()
}
async getUser(req, res) {
const user = await this.userService.getUser(req.params.id)
res.json(user)
}
}
// 依赖流向:Controller -> Service -> Repository -> Model
// 单向依赖,不会形成循环8. 代码中的预防措施
8.1 模块设计原则
单一职责原则 (SRP)
javascript
// 错误:一个模块做太多事情,容易产生循环依赖
// utils.js
import { userStore } from './stores/user.js'
import { postStore } from './stores/post.js'
export const formatUser = (user) => {
/* ... */
}
export const formatPost = (post) => {
/* ... */
}
export const getUserPosts = (userId) => {
const user = userStore.getUser(userId)
const posts = postStore.getUserPosts(userId)
return { user: formatUser(user), posts: posts.map(formatPost) }
}
// 正确:职责分离
// formatters/userFormatter.js
export const formatUser = (user) => {
/* ... */
}
// formatters/postFormatter.js
export const formatPost = (post) => {
/* ... */
}
// services/userPostService.js
import { formatUser } from '../formatters/userFormatter.js'
import { formatPost } from '../formatters/postFormatter.js'
// 服务层组合数据,格式化层只负责格式化依赖倒置原则 (DIP)
javascript
// 错误:高层模块依赖低层模块
// EmailService.js
import { SMTPTransport } from './transports/SMTPTransport.js' // 具体实现
export class EmailService {
constructor() {
this.transport = new SMTPTransport() // 直接依赖具体类
}
}
// 正确:依赖抽象
// interfaces/IEmailTransport.js
export interface IEmailTransport {
send(to: string, subject: string, body: string): Promise<void>
}
// EmailService.js
export class EmailService {
constructor(transport: IEmailTransport) { // 依赖抽象
this.transport = transport
}
}
// transports/SMTPTransport.js
import { IEmailTransport } from '../interfaces/IEmailTransport.js'
export class SMTPTransport implements IEmailTransport {
async send(to: string, subject: string, body: string) {
// SMTP实现
}
}8.2 目录结构设计
src/ ├── types/ # 类型定义,被其他层依赖 │ ├── User.ts │ └── Post.ts ├── constants/ # 常量定义 │ └── api.ts ├── utils/ # 纯函数工具,无外部依赖 │ ├── date.ts │ └── validation.ts ├── repositories/ # 数据访问层 │ ├── UserRepository.ts │ └── PostRepository.ts ├── services/ # 业务逻辑层,依赖repositories │ ├── UserService.ts │ └── PostService.ts ├── stores/ # 状态管理,依赖services │ ├── userStore.ts │ └── postStore.ts ├── hooks/ # React hooks,依赖stores │ ├── useUser.ts │ └── usePost.ts └── components/ # UI组件,依赖hooks ├── UserProfile.tsx └── PostList.tsx
依赖流向:components -> hooks -> stores -> services -> repositories -> types
单向依赖,避免循环
8.3 导入/导出最佳实践
具名导出 vs 默认导出
javascript
// 推荐:具名导出,更明确
// userUtils.js
export const validateUser = (user) => {
/* ... */
}
export const formatUserName = (user) => {
/* ... */
}
// 使用
import { validateUser, formatUserName } from './userUtils.js'
// 避免:默认导出可能导致循环依赖更难发现
// userUtils.js
export default {
validateUser: (user) => {
/* ... */
},
formatUserName: (user) => {
/* ... */
},
}
// 使用时可能不清楚依赖关系
import userUtils from './userUtils.js'Re-export模式
javascript
// 错误:可能导致循环依赖
// index.js
export { userStore } from './userStore.js'
export { postStore } from './postStore.js'
export { commentStore } from './commentStore.js' // commentStore可能依赖index.js
// 正确:分组导出
// stores/index.js
export { userStore } from './userStore.js'
export { postStore } from './postStore.js'
// stores/commentStore.js
export { commentStore } from './commentStore.js'
// 在需要的地方分别导入
import { userStore } from './stores/index.js'
import { commentStore } from './stores/commentStore.js'8.4 TypeScript特定预防措施
接口分离
typescript
// 错误:接口循环依赖
// types/User.ts
import { Post } from './Post.js'
export interface User {
id: string
posts: Post[] // 依赖Post类型
}
// types/Post.ts
import { User } from './User.js' // 循环依赖!
export interface Post {
id: string
author: User // 依赖User类型
}
// 正确:使用ID引用避免类型循环依赖
// types/User.ts
export interface User {
id: string
posts: string[] // 使用PostID而不是Post对象
}
// types/Post.ts
export interface Post {
id: string
authorId: string // 使用UserID而不是User对象
}
// 或者:分离到公共类型文件
// types/common.ts
export interface User {
id: string
posts: string[]
}
export interface Post {
id: string
authorId: string
}
// 在其他地方导入
import { User, Post } from './types/common.js'条件类型避免循环
typescript
// 使用条件类型和泛型避免循环依赖
// types/relations.ts
export type WithRelations<T, R = {}> = T & R
// types/User.ts
export interface BaseUser {
id: string
name: string
}
export type UserWithPosts = WithRelations<
BaseUser,
{
posts: string[] // PostID数组
}
>
// types/Post.ts
export interface BasePost {
id: string
content: string
}
export type PostWithAuthor = WithRelations<
BasePost,
{
authorId: string // UserID
}
>9. 最佳实践和架构设计
9.1 依赖图设计原则
DAG (有向无环图) 原则
javascript
// 良好的依赖图应该是DAG
/*
UI Layer
↓
Business Layer
↓
Data Access Layer
↓
Data Models
*/
// 实际代码结构
// components/ (UI Layer)
// ↓ 依赖
// hooks/ (Business Layer)
// ↓ 依赖
// services/ (Data Access Layer)
// ↓ 依赖
// types/ (Data Models)分层隔离
javascript
// config/layers.js - 定义分层规则
export const LAYERS = {
UI: ['components', 'pages'],
LOGIC: ['hooks', 'stores'],
SERVICE: ['services', 'api'],
DATA: ['types', 'models'],
UTILS: ['utils', 'helpers'],
}
// 分层依赖规则:
// UI -> LOGIC -> SERVICE -> DATA
// UTILS 可以被任何层使用,但不能依赖其他层9.2 模块边界设计
功能模块化
javascript
// 按功能划分模块,而不是按技术类型
// 错误:按技术类型划分
src/
├── components/
│ ├── UserCard.tsx
│ ├── PostCard.tsx
│ └── CommentCard.tsx
├── services/
│ ├── userService.ts
│ ├── postService.ts
│ └── commentService.ts
└── types/
├── User.ts
├── Post.ts
└── Comment.ts
// 正确:按功能划分
src/
├── features/
│ ├── user/
│ │ ├── components/UserCard.tsx
│ │ ├── services/userService.ts
│ │ ├── types/User.ts
│ │ └── index.ts # 统一导出
│ ├── post/
│ │ ├── components/PostCard.tsx
│ │ ├── services/postService.ts
│ │ ├── types/Post.ts
│ │ └── index.ts
│ └── comment/
│ ├── components/CommentCard.tsx
│ ├── services/commentService.ts
│ ├── types/Comment.ts
│ └── index.ts
└── shared/ # 跨功能的公共代码
├── components/
├── utils/
└── types/API设计
javascript
// 每个功能模块暴露清晰的API
// features/user/index.ts
export { UserCard } from './components/UserCard'
export { useUser } from './hooks/useUser'
export type { User } from './types/User'
export { userService } from './services/userService'
// 使用时:
import { UserCard, useUser, type User } from '@/features/user'
// 而不是:
// import { UserCard } from '@/features/user/components/UserCard'
// import { useUser } from '@/features/user/hooks/useUser'9.3 状态管理最佳实践
Store拆分策略
javascript
// 错误:单一巨大的store
// store/index.js
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const appReducer = combineReducers({
user: userReducer, // 用户相关状态
posts: postReducer, // 帖子相关状态
comments: commentReducer, // 评论相关状态
ui: uiReducer, // UI状态
cache: cacheReducer, // 缓存状态
})
// store中的action可能相互调用,容易形成循环依赖
// 正确:按领域拆分store
// stores/userStore.js
export const useUserStore = defineStore('user', () => {
// 只管理用户相关状态
const user = ref(null)
const login = async (credentials) => {
/* */
}
const logout = () => {
/* */
}
return { user, login, logout }
})
// stores/postStore.js
export const usePostStore = defineStore('post', () => {
// 只管理帖子相关状态
const posts = ref([])
const createPost = async (post) => {
/* */
}
return { posts, createPost }
})
// 跨store通信通过事件或组合
// composables/useUserPosts.js
export const useUserPosts = (userId) => {
const userStore = useUserStore()
const postStore = usePostStore()
// 组合多个store的数据
const userPosts = computed(() => {
return postStore.posts.filter((post) => post.authorId === userId)
})
return { userPosts }
}9.4 测试友好的设计
javascript
// 可测试的模块设计避免循环依赖
// services/EmailService.js
export class EmailService {
constructor(transport = new SMTPTransport()) {
// 依赖注入
this.transport = transport
}
async sendEmail(to, subject, body) {
return this.transport.send(to, subject, body)
}
}
// 测试时可以注入mock
// EmailService.test.js
import { EmailService } from './EmailService.js'
const mockTransport = {
send: jest.fn().mockResolvedValue(true),
}
test('should send email', async () => {
const emailService = new EmailService(mockTransport)
await emailService.sendEmail('test@example.com', 'Test', 'Body')
expect(mockTransport.send).toHaveBeenCalled()
})10. 进阶话题
10.1 微前端中的循环依赖
javascript
// 微前端应用间的循环依赖
// 主应用 (Shell App)
// shell/src/store/index.js
import { userStore } from '@microfrontend/user-app' // 依赖子应用
// 子应用 (User App)
// user-app/src/store/userStore.js
import { shellEvents } from '@shell/events' // 依赖主应用 - 循环依赖!
// 解决方案:事件总线 + 契约接口
// shared/events/UserEvents.js
export const USER_EVENTS = {
LOGIN: 'user:login',
LOGOUT: 'user:logout',
PROFILE_UPDATE: 'user:profile-update',
}
// shell/src/eventBus.js
import { EventBus } from '@shared/event-bus'
export const eventBus = new EventBus()
// user-app/src/store/userStore.js
export const userStore = defineStore('user', () => {
const login = (credentials) => {
// 登录逻辑
eventBus.emit(USER_EVENTS.LOGIN, user) // 发送事件,不直接依赖shell
}
})
// shell/src/index.js
eventBus.on(USER_EVENTS.LOGIN, (user) => {
// 处理用户登录事件
})10.2 Monorepo中的循环依赖
javascript
// packages/ui-components/src/UserCard.tsx
import { useUser } from '@company/user-logic' // 依赖业务逻辑包
// packages/user-logic/src/hooks/useUser.ts
import { Button } from '@company/ui-components' // 依赖UI组件包 - 循环依赖!
// 解决方案:分层包设计
/*
packages/
├── foundation/ # 基础层:类型、常量、工具
│ ├── types/
│ ├── constants/
│ └── utils/
├── ui-components/ # UI层:纯UI组件,依赖foundation
│ ├── Button/
│ ├── Input/
│ └── UserCard/
├── business-logic/ # 业务层:hooks、stores,依赖foundation
│ ├── user/
│ ├── post/
│ └── comment/
└── applications/ # 应用层:具体应用,依赖所有其他包
├── web-app/
└── mobile-app/
*/
// 依赖流向:applications -> business-logic -> ui-components -> foundation10.3 动态模块加载
javascript
// 运行时动态解决循环依赖
// moduleRegistry.js
class ModuleRegistry {
constructor() {
this.modules = new Map()
this.loading = new Map()
}
async loadModule(moduleName) {
if (this.modules.has(moduleName)) {
return this.modules.get(moduleName)
}
if (this.loading.has(moduleName)) {
return this.loading.get(moduleName) // 防止重复加载
}
const loadPromise = this.dynamicImport(moduleName)
this.loading.set(moduleName, loadPromise)
try {
const module = await loadPromise
this.modules.set(moduleName, module)
this.loading.delete(moduleName)
return module
} catch (error) {
this.loading.delete(moduleName)
throw error
}
}
async dynamicImport(moduleName) {
switch (moduleName) {
case 'userService':
return import('./services/userService.js')
case 'postService':
return import('./services/postService.js')
default:
throw new Error(`Unknown module: ${moduleName}`)
}
}
}
export const moduleRegistry = new ModuleRegistry()
// 使用
// userComponent.js
import { moduleRegistry } from './moduleRegistry.js'
export const UserComponent = {
async init() {
const { userService } = await moduleRegistry.loadModule('userService')
this.userService = userService
},
}10.4 编译时循环依赖解决
javascript
// 使用编译时宏或转换解决循环依赖
// babel-plugin-resolve-circular.js
module.exports = function () {
return {
visitor: {
ImportDeclaration(path) {
// 检测循环依赖并转换为动态导入
if (detectCircular(path)) {
transformToLazyImport(path)
}
},
},
}
}
// 转换前:
// import { funcA } from './moduleA.js'
// 转换后:
// const getLazyFuncA = () => import('./moduleA.js').then(m => m.funcA)10.5 性能影响和优化
javascript
// 循环依赖对性能的影响
// 1. 模块加载时间增加
// 2. 内存占用增加
// 3. 热更新性能下降
// 4. 打包体积可能增大
// 性能监控
// performanceMonitor.js
export class DependencyPerformanceMonitor {
constructor() {
this.loadTimes = new Map()
this.circularDeps = new Set()
}
recordModuleLoad(moduleName, startTime, endTime) {
this.loadTimes.set(moduleName, endTime - startTime)
}
recordCircularDependency(cycle) {
this.circularDeps.add(cycle.join(' -> '))
}
generateReport() {
return {
slowestModules: [...this.loadTimes.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10),
circularDependencies: [...this.circularDeps],
recommendations: this.generateRecommendations(),
}
}
generateRecommendations() {
const recommendations = []
if (this.circularDeps.size > 0) {
recommendations.push('发现循环依赖,建议重构模块结构')
}
const slowModules = [...this.loadTimes.entries()].filter(([, time]) => time > 100) // 超过100ms的模块
if (slowModules.length > 0) {
recommendations.push('某些模块加载较慢,考虑代码分割或懒加载')
}
return recommendations
}
}** 📋 **总结和检查清单
** 🎯 **关键要点回顾
- 理解本质:循环依赖是模块间相互依赖形成闭环
- 模块系统差异:CommonJS、ESM、AMD处理方式不同
- 构建工具支持:Webpack、Vite都有检测和警告机制
- 多种表现形式:未定义、初始化问题、热更新异常等
- 检测方法:静态分析、运行时监控、专用工具
- 解决策略:依赖注入、事件解耦、分层架构、延迟加载
- 预防措施:良好的模块设计、清晰的架构分层
** ✅ **循环依赖检查清单
设计阶段
- [ ] 确定清晰的模块分层结构
- [ ] 定义模块间的依赖规则
- [ ] 设计API边界和接口契约
- [ ] 选择合适的状态管理方案
开发阶段
- [ ] 配置ESLint的
import/no-cycle规则 - [ ] 使用TypeScript严格模式
- [ ] 遵循单一职责原则
- [ ] 优先使用具名导出
构建阶段
- [ ] 配置Webpack/Vite的循环依赖检测
- [ ] 集成madge等静态分析工具
- [ ] 设置构建失败阈值
- [ ] 生成依赖图可视化
测试阶段
- [ ] 单元测试覆盖模块边界
- [ ] 集成测试验证模块交互
- [ ] 性能测试监控加载时间
- [ ] E2E测试验证完整流程
维护阶段
- [ ] 定期运行循环依赖检测
- [ ] 监控模块加载性能
- [ ] 重构时注意依赖变化
- [ ] 文档记录架构决策 循环依赖是一个复杂但可以避免的问题。通过理解其本质、掌握检测方法、采用正确的解决策略,我们可以构建出更健壮、更易维护的应用架构。记住:预防胜于治疗,良好的架构设计是避免循环依赖的最佳方式!
